/**
 * 
 */
package qy.jalgotrade.feed;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;

import qy.jalgotrade.dataseries.DataSeries;
import qy.jalgotrade.event.Event;
import qy.jalgotrade.event.Subject;
import qy.jalgotrade.utils.PySpecs;

/**
 * <pre>
 * Base class for feeds.
 * 
 * .. note::
 *     This is a base class and should not be used directly.
 * </pre>
 * 
 * @author c-geo
 *
 */
public abstract class BaseFeed<T> extends Subject implements PySpecs.Contains<String>,
        PySpecs.Iter<Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>>>, PySpecs.MapGetItem<String, DataSeries<T>> {

	private Map<String, DataSeries<T>> __ds;

	private Event __newValueEvent;

	private Event __newValueReadyEvent;

	private int __maxLen;

	private Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> __dispatching;

	/**
	 * 
	 * @param maxLen The maximum number of values that each :class:`pyalgotrade.dataseries.DataSeries`
	 *               will hold. Once a bounded length is full, when new items are added, a corresponding
	 *               number of items are discarded from the opposite end.
	 * @throws Exception
	 */
	public BaseFeed(int maxLen) throws Exception {

		super();

		__maxLen = DataSeries.getCheckedMaxLen(maxLen);

		__ds = new HashMap<>();
		__newValueEvent = new Event("feed-new-values-event");
		__newValueReadyEvent = new Event("feed-new-values-ready-event");
		__dispatching = null;
	}

	public void reset() throws Exception {

		assert !isDispatching() : "Can NOT reset() when Subject is dispatching!";

		List<String> keys = new ArrayList<>(__ds.keySet());
		__ds = new HashMap<>();
		for (String key : keys) {
			registerDataSeries(key);
		}
	}

	/**
	 * Subclasses should implement this and return the appropriate dataseries for the given key.
	 * 
	 * @param key
	 * @param maxLen
	 * @return
	 * @throws Exception
	 */
	public abstract DataSeries<T> createDataSeries(String key, int maxLen) throws Exception;

	/**
	 * <pre>
	 * Subclasses should implement this and return a tuple with two elements:
	 * 1: datetime.datetime.
	 * 2: dctionary or dict-like object.
	 * 
	 * 返回值为一个 tuple:
	 * 1. datetime.datetime;
	 * 2. dict 或 dict-like object.
	 * </pre>
	 * 
	 * @return
	 */
	public abstract Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> getNextValues() throws Exception;

	public void registerDataSeries(String key) throws Exception {

		assert !isDispatching() : "Can NOT registerDataSeries() when Subject is dispatching!";

		if (!__ds.containsKey(key)) {
			__ds.put(key, createDataSeries(key, __maxLen));
		}
	}

	/**
	 * 
	 * @return
	 * @throws Exception
	 */
	public Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> getNextValuesAndUpdateDS() throws Exception {

		Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> p = getNextValues();
		ZonedDateTime dateTime = p.getLeft();
		PySpecs.ReadOnlyMap<String, T> values = p.getRight();

		if (dateTime != null) {

			Iterator<Pair<String, T>> itr = values.__iter__();
			while (itr.hasNext()) {

				Pair<String, T> item = itr.next();
				String key = item.getKey();
				T value = item.getValue();

				// Get or create the datseries for each key.
				DataSeries<T> ds = __ds.get(key);
				if (ds == null) {
					ds = createDataSeries(key, __maxLen);
					__ds.put(key, ds);
				}
				ds.appendWithDateTime(dateTime, value);
			}
		}
		return p;
	}

	/**
	 * <pre>
	 * 当 new values 可用时, 先生成 feed-new-values-ready-event, 用来更新其他 Subject, 如基于此的 ResampledBarFeed,
	 * 将获取到的 (datetime, value) 缓存在 __dispatching 中, 然后生成 feed-new-values-event.
	 * </pre>
	 * 
	 * <pre>
	 * Returns the event that will be emitted when new values are available. To subscribe you need to
	 * pass in a callable object that receives two parameters:
	 * 
	 * 1. A :class:`datetime.datetime` instance.
	 * 2. The new value.
	 * </pre>
	 * 
	 * @return
	 */
	public Event getNewValuesReadyEvent() {

		return __newValueReadyEvent;
	}

	/**
	 * <pre>
	 * Returns the event that will be emitted when new values are available. To subscribe you need to
	 * pass in a callable object that receives two parameters:
	 * 
	 * 1. A :class:`datetime.datetime` instance.
	 * 2. The new value.
	 * </pre>
	 * 
	 * @return
	 */
	public Event getNewValuesEvent() {

		return __newValueEvent;
	}

	/*
	 * <pre>
	 * 当 new values 可用时, 先生成 feed-new-values-ready-event, 用来更新其他 Subject, 如基于此的 ResampledBarFeed,
	 * 将获取到的 (datetime, value) 缓存在 __dispatching 中, 然后生成 feed-new-values-event.
	 * </pre>
	 * (non-Javadoc)
	 * @see qy.jalgotrade.event.Subject#preDispatch()
	 */
	@Override
	public void preDispatch() throws Exception {

		Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> p = getNextValuesAndUpdateDS();
		ZonedDateTime dateTime = p.getLeft();
		PySpecs.ReadOnlyMap<String, T> values = p.getRight();
		if (dateTime != null) {

			__dispatching = p;

			Map<String, Object> params = new HashMap<>();
			params.put("dateTime", dateTime);
			params.put("values", values);
			__newValueReadyEvent.emit(__newValueReadyEvent.getName(), params);
		}
	}

	/*
	 * Subject 具体实现的 dispatch() 入口: 1. 获取下一个可用的 Value 并更新 时间序列: self.getNextValuesAndUpdateDS(); 2.
	 * 生成 Event (传入 datetime 和 Value).
	 * 
	 * (non-Javadoc)
	 * 
	 * @see qy.jalgotrade.event.Subject#dispatch()
	 */
	@Override
	public boolean dispatch() throws Exception {

		try {
			if (__dispatching != null) {
				ZonedDateTime dateTime = __dispatching.getLeft();
				PySpecs.ReadOnlyMap<String, T> values = __dispatching.getRight();

				Map<String, Object> params = new HashMap<>();
				params.put("dateTime", dateTime);
				params.put("values", values);
				__newValueEvent.emit(__newValueEvent.getName(), params);
			}

			return __dispatching != null;

		} finally {
			__dispatching = null;
		}
	}

	public List<String> getKeys() {

		return new ArrayList<>(__ds.keySet());
	}

	/**
	 * key: 标的代码, 检查是否包含;<br />
	 * Returns True if a :class:`pyalgotrade.dataseries.DataSeries` for the given key is available.
	 * 
	 * @param key
	 * @return
	 */
	@Override
	public boolean __contains__(String key) {

		return __ds.containsKey(key);
	}

	/**
	 * key: 标的代码, 返回指定 标的 DataSeries;<br />
	 * Returns the :class:`pyalgotrade.dataseries.DataSeries` for a given key.
	 * 
	 * @param key
	 * @return
	 */
	@Override
	public DataSeries<T> __getitem__(String key) {

		return __ds.get(key);
	}

	/**
	 * 注意: 使用 __iter__() 返回的迭代器, 不支持基于当前 BarFeed 的 ResampledBarFeed.
	 * 
	 * @return
	 */
	@Override
	public Iterator<Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>>> __iter__() {

		assert !isDispatching() : "Can NOT __iter__() when Subject is dispatching!";

		return new Iterator<Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>>>() {

			BaseFeed<T> feed = BaseFeed.this;

			// init:
			{
				feed.start();
			}

			@Override
			public boolean hasNext() {

				return !feed.eof();
			}

			@Override
			public Pair<ZonedDateTime, PySpecs.ReadOnlyMap<String, T>> next() {

				try {

					return feed.getNextValuesAndUpdateDS();

				} catch (Exception e) {

					throw new NoSuchElementException(
					        String.format("Exception: %s, (stack trace: %s)", e, ExceptionUtils.getStackTrace(e)));

				} finally {
					if (feed.eof()) {
						feed.stop();
						feed.join();
					}
				}
			}
		};
	}
}
